Skip to content

fix(sleep): displaySmoothed(minDuration<=0) coalesces like Swift instead of returning raw fragments (#240 follow-up) - #316

Merged
ryanbr merged 1 commit into
mainfrom
sleep-smoothing-port-parity
Jul 12, 2026
Merged

ryanbr merged 1 commit into
mainfrom
sleep-smoothing-port-parity

Conversation

@ryanbr

@ryanbr ryanbr commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Follow-up to #240 (merged). Fixes the one port-fidelity divergence the review found.

The divergence

The Android port of Swift Hypnogram.displaySmoothed added if (intervals.size <= 2 || minDurationSec <= 0.0) return intervals. Swift guards only on count and lets minDuration = 0 ("raw") fall through to coalesce() — so Swift returns the coalesced timeline, whereas Android returned the 60–100 un-merged epoch fragments (the "comb" the smoothing exists to remove). Everything else in the port matches Swift line-for-line.

Severity

Production-inert: the only caller passes STAGE_ROW_SMOOTH_SEC = 90.0, so the divergent <= 0 path never runs in shipping. This is a latent fidelity fix — it makes the "straight port" true and protects any future 0-caller.

Fix

  • Drop the || minDurationSec <= 0.0 short-circuit. With minDuration = 0, the absorb loop's duration < 0 filter is empty, so it breaks right after the first coalesce() — exactly Swift's result.
  • Strengthen the test: zeroFloorReturnsCoalescedInputUnsmoothed previously used [light, deep, rem] (no adjacent same-stage → coalesce a no-op), so it passed even with the short-circuit. New fixture has a light/light seam and asserts the coalesced output. Verified sensitive: it fails against the old short-circuit, passes with the fix.

Android-only; testFullDebugUnitTest green.

… not return raw fragments (#240 follow-up)

The Android port of Swift Hypnogram.displaySmoothed added an early return
`if (intervals.size <= 2 || minDurationSec <= 0.0) return intervals`. The
`|| minDurationSec <= 0.0` short-circuit isn't in Swift, which guards ONLY on count
and lets the "raw" (minDuration 0) case fall through to coalesce() — so Swift returns
the coalesced timeline, while Android returned the 60-100 un-merged epoch fragments
(the "comb" the smoothing exists to remove). Everything else in the port is faithful.

Production is unaffected (the only caller passes STAGE_ROW_SMOOTH_SEC = 90.0), so this
is a latent port-fidelity fix, not a shipping bug — but it makes the "straight port"
claim true and protects a future 0-caller.

Also strengthens `zeroFloorReturnsCoalescedInputUnsmoothed`: its old [light, deep, rem]
fixture had no adjacent same-stage runs, so coalesce was a no-op and it passed even
with the short-circuit. New fixture has a light/light seam and asserts the coalesced
result — verified it FAILS against the old short-circuit and passes with the fix.

Android-only (Health Connect / SleepScreen); testFullDebugUnitTest green.
@ryanbr
ryanbr merged commit 29db8ed into main Jul 12, 2026
@ryanbr
ryanbr deleted the sleep-smoothing-port-parity branch July 12, 2026 06:50
DX23876 pushed a commit to DX23876/noop that referenced this pull request Jul 22, 2026
… not return raw fragments (ryanbr#240 follow-up) (ryanbr#316)

The Android port of Swift Hypnogram.displaySmoothed added an early return
`if (intervals.size <= 2 || minDurationSec <= 0.0) return intervals`. The
`|| minDurationSec <= 0.0` short-circuit isn't in Swift, which guards ONLY on count
and lets the "raw" (minDuration 0) case fall through to coalesce() — so Swift returns
the coalesced timeline, while Android returned the 60-100 un-merged epoch fragments
(the "comb" the smoothing exists to remove). Everything else in the port is faithful.

Production is unaffected (the only caller passes STAGE_ROW_SMOOTH_SEC = 90.0), so this
is a latent port-fidelity fix, not a shipping bug — but it makes the "straight port"
claim true and protects a future 0-caller.

Also strengthens `zeroFloorReturnsCoalescedInputUnsmoothed`: its old [light, deep, rem]
fixture had no adjacent same-stage runs, so coalesce was a no-op and it passed even
with the short-circuit. New fixture has a light/light seam and asserts the coalesced
result — verified it FAILS against the old short-circuit and passes with the fix.

Android-only (Health Connect / SleepScreen); testFullDebugUnitTest green.
ryanbr added a commit that referenced this pull request Jul 26, 2026
Swept their decoder against every v18 byte we still carry as raw, rather than only
the ones they advertise in their README. Three results.

@40 gets a name. They decode it as `signal_quality`. Plausible for a byte sitting
beside the HR fields, but it appears only in their code — no README entry, no
source tag, no supporting analysis — so it stays raw here and is recorded as a lead
rather than adopted. Noting it so the next person looking at that byte starts with
a hypothesis instead of nothing.

@63 is corroborated. They decode the same offset as an activity class with the same
`<= 2` gate we use (#316), reached independently. That is a second source on a field
we had from one.

The SpO2 gate is evidence. Their code clamps byte 82 to 70..100 and drops anything
outside instead of storing it unconditionally. Code shaped that way implies its
author saw values inside the band and values outside it worth rejecting — which is
what a real percentage with invalid samples looks like, and not what a 0-3 state
code looks like. Recorded as suggestive, not decisive: a gate can also be purely
defensive.

Nothing adopted, nothing decoded. Their remaining unmatched offsets checked out as
coincidental number collisions, not claims.

Deliberately NOT touching the Credits table: it documents projects whose findings
this codebase PORTS, and we have ported nothing from whoop-local — the inline #715
citations are the accurate form of credit here.
ryanbr added a commit that referenced this pull request Jul 26, 2026
…ion claim, record the byte-82 lead (#520, #715)

Docs and test fixture only. No decoder change, no schema, no scores, no strings.

BYTE 43 IS NOT A RESPIRATION RATE (#520)

A third-party decoder reads a u8 at payload offset 35 — frame offset 43 — as a raw
respiration rate. It is the third byte of the little-endian dynamic_acceleration
float32 at 41. Disproved against the six real v18 frames already in the repo:

  frame                        byte43 as "brpm"  f32@41 (g)  |g| @45/49/53
  whoop5_v18_real_worn                       22      0.0092         1.0086
  whoop5_v18_real_one_rr                     47      0.0107         1.0106
  whoop5_v18_real_offwrist                  195      0.0060         0.9985
  whoop5_v18_real_ack_capture               108      0.0144         1.0074
  whoop5_v18_real_device2_hr57                6      0.0328         1.0029
  whoop5_v18_real_device2_hr63                9      0.0084         1.0096

The evidence in order: the f32s at 45/49/53 give |g| = 0.9985..1.0106 on every
frame, which a misaligned read does not produce, so the 4-byte grid is fixed; read
as an f32 on that grid, 41-44 is physically coherent across worn, off-wrist and two
straps; read as a rate, byte 43 is impossible — worst on the OFF-WRIST frame, 195
breaths/min from a strap nobody is wearing with heart_rate@22 = 0. A byte belongs to
one field.

The doc states the tautology to avoid: byte 43 IS byte 2 of that float by
construction, so reproducing it from the exponent/mantissa bits proves nothing. The
argument is the gravity anchor plus the off-wrist value.

dynamic_acceleration is now pinned in decoder_oracle.json on both platforms, so a
future offset change fails a test rather than silently reintroducing a fabricated
vital sign. No new test code needed — both harnesses already assert arbitrary double
keys within tolerance, and testOracleCopiesAreIdentical keeps the copies in sync.

Also adds the @41 row to the v18 table. The field has been decoded on both platforms
all along but had no entry in the spec, which is part of why an outside reader could
mistake one of its bytes for a channel of its own.

BYTE 82 MAY BE A SLEEP-ONLY SpO2 % (#715) — OPEN

whoop-local reads byte 82 as WHOOP's own computed SpO2 percentage, sleep-only,
attributing it to hardware RE rather than app decompilation. That fits an
observation already in our table and never explained: aux_byte_82 is nonzero only
while sleep_state = asleep.

It cannot be confirmed here, and the evidence base is thinner than a grep suggests:
there are six DISTINCT v18 frames in the tree, from two straps, recurring across 2-7
files each because the Swift and Kotlin suites share fixtures — a naive count
returns 21 and overstates the sample 3.5x. All six are awake with byte 82 = 0, which
is consistent with a sleep-only channel and with several other readings.

Recorded as an open hypothesis; the byte stays raw and unnamed. One asleep record
settles it: 90-100 is a percentage, 0-3 a state code. The doc also names the likely
shortcut — the out-of-tree ~258k-record corpus this file cites elsewhere for
hr_fixed_8_8@36 and for status_word@75 "occurring as often awake as asleep" must
contain asleep records, so one query against it resolves this without a new capture.

CROSS-CHECK FINDINGS

v20 optical, independently corroborated: whoop-local lists six 20-bit channel
offsets derived separately — frame 47, 247, 1313, 1513, 1735, 1935 — which are
exactly the six slots of our non-empty blocks (0, 3, 4; the corpus shows counts are
always [25, 0, 0, 25, 25]) out of the ten this document lists. Two unrelated methods,
same six. The doc says explicitly that this confirms STRUCTURE only — neither project
has a labelled capture, so the v20 sensor-identity question is untouched.

@63 corroborated: they decode the same offset as an activity class with the same
<= 2 gate we use (#316), reached independently.

@40 gets a name: they call it signal_quality. Plausible beside the HR fields, but it
appears only in their code with no source tag and no analysis, so it stays raw here
and is recorded as a lead.

Their SpO2 gate is itself evidence: the code clamps byte 82 to 70..100 and drops
anything outside rather than storing it, which implies the author saw values inside
that band and values outside worth rejecting. Suggestive, not decisive.

Nothing was adopted from whoop-local. Its primary source is the decompiled WHOOP
Android app (com/whoop/service/rearchitect/c.java, zi0/*, bj0/x.java, qh0/*), which
this project's clean-room rule rules out regardless of correctness. The findings used
here are the ones it attributes to hardware RE, and each is recorded as a lead to
verify rather than a fact to copy. The Credits table is deliberately untouched: it
documents projects whose findings this codebase PORTS, and nothing was ported.
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
… not return raw fragments (ryanbr#240 follow-up) (ryanbr#316)

The Android port of Swift Hypnogram.displaySmoothed added an early return
`if (intervals.size <= 2 || minDurationSec <= 0.0) return intervals`. The
`|| minDurationSec <= 0.0` short-circuit isn't in Swift, which guards ONLY on count
and lets the "raw" (minDuration 0) case fall through to coalesce() — so Swift returns
the coalesced timeline, while Android returned the 60-100 un-merged epoch fragments
(the "comb" the smoothing exists to remove). Everything else in the port is faithful.

Production is unaffected (the only caller passes STAGE_ROW_SMOOTH_SEC = 90.0), so this
is a latent port-fidelity fix, not a shipping bug — but it makes the "straight port"
claim true and protects a future 0-caller.

Also strengthens `zeroFloorReturnsCoalescedInputUnsmoothed`: its old [light, deep, rem]
fixture had no adjacent same-stage runs, so coalesce was a no-op and it passed even
with the short-circuit. New fixture has a light/light seam and asserts the coalesced
result — verified it FAILS against the old short-circuit and passes with the fix.

Android-only (Health Connect / SleepScreen); testFullDebugUnitTest green.
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
…ion claim, record the byte-82 lead (ryanbr#520, ryanbr#715)

Docs and test fixture only. No decoder change, no schema, no scores, no strings.

BYTE 43 IS NOT A RESPIRATION RATE (ryanbr#520)

A third-party decoder reads a u8 at payload offset 35 — frame offset 43 — as a raw
respiration rate. It is the third byte of the little-endian dynamic_acceleration
float32 at 41. Disproved against the six real v18 frames already in the repo:

  frame                        byte43 as "brpm"  f32@41 (g)  |g| @45/49/53
  whoop5_v18_real_worn                       22      0.0092         1.0086
  whoop5_v18_real_one_rr                     47      0.0107         1.0106
  whoop5_v18_real_offwrist                  195      0.0060         0.9985
  whoop5_v18_real_ack_capture               108      0.0144         1.0074
  whoop5_v18_real_device2_hr57                6      0.0328         1.0029
  whoop5_v18_real_device2_hr63                9      0.0084         1.0096

The evidence in order: the f32s at 45/49/53 give |g| = 0.9985..1.0106 on every
frame, which a misaligned read does not produce, so the 4-byte grid is fixed; read
as an f32 on that grid, 41-44 is physically coherent across worn, off-wrist and two
straps; read as a rate, byte 43 is impossible — worst on the OFF-WRIST frame, 195
breaths/min from a strap nobody is wearing with heart_rate@22 = 0. A byte belongs to
one field.

The doc states the tautology to avoid: byte 43 IS byte 2 of that float by
construction, so reproducing it from the exponent/mantissa bits proves nothing. The
argument is the gravity anchor plus the off-wrist value.

dynamic_acceleration is now pinned in decoder_oracle.json on both platforms, so a
future offset change fails a test rather than silently reintroducing a fabricated
vital sign. No new test code needed — both harnesses already assert arbitrary double
keys within tolerance, and testOracleCopiesAreIdentical keeps the copies in sync.

Also adds the @41 row to the v18 table. The field has been decoded on both platforms
all along but had no entry in the spec, which is part of why an outside reader could
mistake one of its bytes for a channel of its own.

BYTE 82 MAY BE A SLEEP-ONLY SpO2 % (ryanbr#715) — OPEN

whoop-local reads byte 82 as WHOOP's own computed SpO2 percentage, sleep-only,
attributing it to hardware RE rather than app decompilation. That fits an
observation already in our table and never explained: aux_byte_82 is nonzero only
while sleep_state = asleep.

It cannot be confirmed here, and the evidence base is thinner than a grep suggests:
there are six DISTINCT v18 frames in the tree, from two straps, recurring across 2-7
files each because the Swift and Kotlin suites share fixtures — a naive count
returns 21 and overstates the sample 3.5x. All six are awake with byte 82 = 0, which
is consistent with a sleep-only channel and with several other readings.

Recorded as an open hypothesis; the byte stays raw and unnamed. One asleep record
settles it: 90-100 is a percentage, 0-3 a state code. The doc also names the likely
shortcut — the out-of-tree ~258k-record corpus this file cites elsewhere for
hr_fixed_8_8@36 and for status_word@75 "occurring as often awake as asleep" must
contain asleep records, so one query against it resolves this without a new capture.

CROSS-CHECK FINDINGS

v20 optical, independently corroborated: whoop-local lists six 20-bit channel
offsets derived separately — frame 47, 247, 1313, 1513, 1735, 1935 — which are
exactly the six slots of our non-empty blocks (0, 3, 4; the corpus shows counts are
always [25, 0, 0, 25, 25]) out of the ten this document lists. Two unrelated methods,
same six. The doc says explicitly that this confirms STRUCTURE only — neither project
has a labelled capture, so the v20 sensor-identity question is untouched.

@63 corroborated: they decode the same offset as an activity class with the same
<= 2 gate we use (ryanbr#316), reached independently.

@40 gets a name: they call it signal_quality. Plausible beside the HR fields, but it
appears only in their code with no source tag and no analysis, so it stays raw here
and is recorded as a lead.

Their SpO2 gate is itself evidence: the code clamps byte 82 to 70..100 and drops
anything outside rather than storing it, which implies the author saw values inside
that band and values outside worth rejecting. Suggestive, not decisive.

Nothing was adopted from whoop-local. Its primary source is the decompiled WHOOP
Android app (com/whoop/service/rearchitect/c.java, zi0/*, bj0/x.java, qh0/*), which
this project's clean-room rule rules out regardless of correctness. The findings used
here are the ones it attributes to hardware RE, and each is recorded as a lead to
verify rather than a fact to copy. The Credits table is deliberately untouched: it
documents projects whose findings this codebase PORTS, and nothing was ported.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant